package pl.edu.pb.wi.pwnography.modules;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

public class OutlierDetection {
    private static final Logger log = Logger.getLogger(OutlierDetection.class
	    .getName());
    
    private static final String PREFIX_BASE = "OUTLIER_DETECTION_";
    private static final String STANDARD_DEVIATION_PREFIX = "3X_STANDARD_DEVIATION_";
    private static final String POSITIVE_RESPONSE = "TRUE";
    private static final String NEGATIVE_RESPONSE = "FALSE";
    private static final String PERCENT_PREFIX = "PERCENT_";

    public static Map<String, List<Object>> standardDeviation(
	    String columnName, List<Object> columnValues, String newColumnName)
	    throws Exception {
	if (columnValues.size() < 1) {
	    return null;
	}

	/*
	 * Sprawdzenie czy każda z wartości kolumny da się zrzutować na typ
	 * Float. Przy okazji zlicz sumę wszystkich wartości.
	 */
	Float sum = 0f;
	for (Object o : columnValues) {
	    if (!(o instanceof Float)) {
		throw new Exception(
			String.format(
				"Value [%s] isn't Float type! Fix column [%s]. Normalization can be used only on Float type columns.",
				o.toString(), columnName));
	    } else {
		sum += (Float) o;
	    }
	}

	Float standardDeviation = calculateStandardDeviation(columnValues, sum);
	Float average = sum / columnValues.size();

	/*
	 * Porównanie każdej różnicy wartości i średniej z wartości z potrojoną
	 * wartością odchylenia standardowego.
	 */
	Float trippleStandardDeviation = 3 * standardDeviation;
	List<Object> detectionColumn = new LinkedList<Object>();
	for (Object o : columnValues) {
	    Float diff = Math.abs(average - (Float) o);
	    if (diff > trippleStandardDeviation) {
		detectionColumn.add(POSITIVE_RESPONSE);
	    } else {
		detectionColumn.add(NEGATIVE_RESPONSE);
	    }
	}

	/*
	 * Tworzenie nazwy nowej kolumny oraz umieszczenie jej razem z
	 * wartościami w HashMapie.
	 */
	String prefixedName;

	if (newColumnName == null || newColumnName.isEmpty()) {
	    prefixedName = new StringBuilder(PREFIX_BASE)
		    .append(STANDARD_DEVIATION_PREFIX).append(columnName)
		    .toString();
	} else {
	    prefixedName = newColumnName;
	}

	Map<String, List<Object>> namedDetectedColumn = new LinkedHashMap<String, List<Object>>();
	namedDetectedColumn.put(prefixedName, detectionColumn);

	return namedDetectedColumn;
    }

    public static Float calculateStandardDeviation(List<Object> columnValues,
	    Float sum) {
	/*
	 * Średnia.
	 */
	Integer count = columnValues.size();
	Float average = sum / count;

	/*
	 * Suma różnic wszystkich wartości oraz średniej podniesiona do
	 * kwadratu.
	 */
	Float averageValueDiff = 0f;
	for (Object o : columnValues) {
	    averageValueDiff += (float) Math.pow(((Float) o - average), 2);
	}

	/*
	 * Wyliczenie wariancji oraz odchylenia standardowego.
	 */
	Float variance = averageValueDiff / count;
	Float standardDeviation = (float) Math.sqrt(variance);

	log.info(String
		.format("Count: %d\n Average: %f\n Average value difference: %f\n Variance: %f\n Standard deviation: %f\n",
			count, average, averageValueDiff, variance,
			standardDeviation));

	return standardDeviation;
    }

    public static Map<String, List<Object>> percentage(String columnName,
	    List<Object> columnValues, Float percent, String newColumnName)
	    throws Exception {
	if (columnValues.size() < 1) {
	    return null;
	}

	/*
	 * Sprawdzenie czy każda z wartości kolumny da się zrzutować na typ
	 * Float.
	 */
	for (Object o : columnValues) {
	    if (!(o instanceof Float)) {
		throw new Exception(
			String.format(
				"Value [%s] isn't Float type! Fix column [%s]. Normalization can be used only on Float type columns.",
				o.toString(), columnName));
	    }
	}

	Float percentage = percent / Float.valueOf(100);
	Integer numberOfExtremes = Math.round(columnValues.size() * percentage);

	List<Object> resultColumn = new ArrayList<Object>(columnValues);
	Collections.fill(resultColumn, Boolean.FALSE.toString());
	if (numberOfExtremes > 0) {
	    ArrayList<Object> list = new ArrayList<Object>(columnValues);
	    Collections.sort(list, new Comparator<Object>() {
		@Override
		public int compare(Object o1, Object o2) {
		    Float first = (Float) o1;
		    Float second = (Float) o2;
		    return first.compareTo(second);
		}
	    });

	    Float minimum = (Float) list.get(numberOfExtremes - 1);
	    Float maximum = (Float) list.get(list.size() - numberOfExtremes);

	    for (int i = 0; i < columnValues.size(); ++i) {
		if ((Float) columnValues.get(i) >= maximum
			|| (Float) columnValues.get(i) <= minimum)
		    resultColumn.set(i, Boolean.TRUE.toString());
	    }
	}

	/*
	 * Tworzenie nazwy nowej kolumny oraz umieszczenie jej razem z
	 * wartościami w HashMapie.
	 */
	String prefixedName;

	if (newColumnName == null || newColumnName.isEmpty()) {
	    prefixedName = new StringBuilder(PREFIX_BASE)
		    .append(PERCENT_PREFIX).append(columnName).append("_")
		    .append(percent).append("%").toString();
	} else {
	    prefixedName = newColumnName;
	}

	Map<String, List<Object>> namedPercentColumn = new LinkedHashMap<String, List<Object>>();
	namedPercentColumn.put(prefixedName, resultColumn);

	return namedPercentColumn;
    }
}
